﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TeamArchitect.ModelingProject;
using Microsoft.VisualStudio.Data;
using System.IO;
using TFDP.Common;
using EnvDTE;
using System.Diagnostics;
using System.Collections;
using System.Xml;
using System.Xml.XPath;
using TFDP.VisualStudioInterop;

namespace TFDP.VsPackage.Support.TargetProjects
{
    public sealed class ModelingProject : ITargetProject
    {
        private IVsHierarchy projectHierarchy;
        private ModelingProjectNode project;
        private IVsQueryEditQuerySave2 queryEditService;

        #region ITargetProject Members

        public void Initialize(Microsoft.VisualStudio.Shell.Interop.IVsHierarchy projectHierarchy)
        {
            this.projectHierarchy = projectHierarchy;
            this.project = VsInteropUtilities.GetModelingProject(projectHierarchy);

            EnvDTE.Project targetProject = VsInteropUtilities.GetEnvDTEProject(projectHierarchy);

            IServiceProvider serviceProvider = new ServiceProvider(
                (Microsoft.VisualStudio.OLE.Interop.IServiceProvider)targetProject.DTE
            );

            queryEditService = (IVsQueryEditQuerySave2)serviceProvider.GetService(typeof(SVsQueryEditQuerySave));
        }

        public bool Update(Common.GeneratedArtifact artifact)
        {
            if (artifact == null)
                throw new ArgumentNullException("artifact");

            string projectDirectory = Path.GetDirectoryName(VsInteropUtilities.GetFileNameFromHierarchy(projectHierarchy));
            string targetAbsolutePath = Path.Combine(projectDirectory, artifact.TargetFileName);
            string targetRelativePath = Path.GetDirectoryName(artifact.TargetFileName).Replace(projectDirectory, String.Empty);

            // Split the relative path into directories
            string[] pathParts = targetRelativePath.Split(
                new char[] { Path.DirectorySeparatorChar },
                StringSplitOptions.RemoveEmptyEntries
            );

            // Check if the target folders exist, starting at the root of the project
            // If the target for the artifact is the root of the project, the pathParts
            // array will be empty. If that's the case just take the project root (first item)
            EnvDTE.ProjectItems targetFolder = null;
            if (pathParts.Length > 0)
            {
                targetFolder = project.Project.ProjectItems.Item(pathParts[0]).ProjectItems as EnvDTE.ProjectItems;
            }
            else
            {
                targetFolder = project.Project.ProjectItems;
            }

            // Index of ProjectItems starts at 1 not 0
            for (int i = 1; i < pathParts.Length; i++)
            {
                targetFolder = targetFolder.Item(pathParts[i]).ProjectItems;

                if (targetFolder == null)
                {
                    Logger.LogError(
                        String.Format(
                            "Part of the path {0} does not exist. Missing directory {1}",
                            targetRelativePath,
                            pathParts[i]
                        )
                    );

                    return false;
                }
            }

            // Check if the file exists
            bool targetExists = false;
            ProjectItem targetItem = null;

            if (targetFolder.Count > 0)
            {
                var matches = targetFolder.OfType<EnvDTE.ProjectItem>()
                    .Where(p => p.Name == Path.GetFileName(artifact.TargetFileName))
                    .Select(p => p);

                if (matches.Count() > 0)
                {
                    targetItem = matches.First();
                }

                targetExists = (targetItem != null);
            }
            else
                targetExists = false;

            if (targetExists)
            {
                FileEditRequestResult result = queryEditService.CanEdit(artifact.TargetFileName);
                if (!result.Success)
                {
                    Logger.LogExtended(TraceLevel.Error, String.Format("Unable to write file: {0}", result.Message), artifact.TargetFileName, 1, 1);
                    return false;
                }
                
                // Regular edit
                File.Copy(artifact.TemporaryFileName, targetAbsolutePath, true);
            }
            else if (!targetExists)
            {
                // Copy the temporary file to the target location
                // and add it to the project at the specified 
                // folder
                File.Copy(artifact.TemporaryFileName, targetAbsolutePath, true);
                targetFolder.AddFromFile(targetAbsolutePath);
            }

            SynchronizeModel(artifact);

            return true;
        }
        #endregion

        private const string ns = "http://schemas.microsoft.com/dsltools/ModelStore";

        private void SynchronizeModel(GeneratedArtifact artifact)
        {
            XmlDocument artifactDocument = new XmlDocument();
            artifactDocument.Load(artifact.TemporaryFileName);

            XmlDocument modelDocument = new XmlDocument();
            string modelPath = Path.Combine(project.ProjectFolder, "ModelDefinition", String.Format("{0}.uml", project.Caption));
            modelDocument.Load(modelPath);

            XmlNamespaceManager mgr = new XmlNamespaceManager(new NameTable());
            mgr.AddNamespace("foo", ns);
            XmlNodeList nodes = artifactDocument.DocumentElement.SelectNodes("/foo:package", mgr);

            foreach(XmlNode node in nodes)
            {
                string packageName = node.Attributes["name"].Value;
                string packageGuidString = node.Attributes["Id"].Value;

                SynchronizeClassToModel(modelDocument, packageName, packageGuidString);
            }

            modelDocument.Save(modelPath);
        }

        private static void SynchronizeClassToModel(
            XmlDocument modelDocument, 
            string packageName,
            string packageGuidString)
        {
            XmlNamespaceManager mgr = new XmlNamespaceManager(new NameTable());
            mgr.AddNamespace("foo", ns);
            string xpath = String.Format("//packageHasNamedElement/packageMoniker[@LastKnownName='{0}']", packageName);

            XmlNodeList nodes = modelDocument.DocumentElement.SelectNodes(xpath, mgr);

            if (nodes == null || nodes.Count == 0)
            {
                XmlElement umlClass = modelDocument.CreateElement("packageMoniker", ns);

                XmlAttribute idAttribute = modelDocument.CreateAttribute("Id");
                idAttribute.Value = packageGuidString;
                umlClass.Attributes.Append(idAttribute);

                XmlAttribute lastKnownNameAttribute = modelDocument.CreateAttribute("LastKnownName");
                lastKnownNameAttribute.Value = packageName;
                umlClass.Attributes.Append(lastKnownNameAttribute);

                XmlAttribute lastKnownLocation = modelDocument.CreateAttribute("LastKnownLocation");
                lastKnownLocation.Value = packageName + ".uml";
                umlClass.Attributes.Append(lastKnownLocation);

                XmlNode packagedElements = modelDocument.SelectSingleNode("//foo:packagedElements", mgr);
                if (packagedElements == null)
                {
                    packagedElements = modelDocument.CreateElement("packagedElements", ns);
                    //modelDocument.DocumentElement.AppendChild(packagedElements);
                    modelDocument.DocumentElement.InsertAfter(packagedElements, modelDocument.DocumentElement.SelectSingleNode("//foo:profileInstances", mgr));
                }
                XmlNode newChild = modelDocument.CreateElement("packageHasNamedElement", ns);
                newChild.AppendChild(umlClass);

                packagedElements.AppendChild(newChild);
            }
            else
            {
                nodes[0].Attributes["Id"].Value = packageGuidString;

                if (nodes.Count > 1)
                {
                    XmlNode parent = nodes[0].ParentNode.ParentNode;

                    for (int i = 1; i < nodes.Count; i++)
                    {
                        parent.RemoveChild(nodes[i].ParentNode);
                    }
                }
            }
        }
    }
}
